package com.tlgen.formlogin6.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.tlgen.formlogin6.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.session.HttpSessionEventPublisher;

import java.io.PrintWriter;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    UserService userService;

    @Bean
    PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    /**
     * 通过监听 session 的销毁事件，来及时的清理 session 的记录，用户从不同的浏览器登录后，都会有对应的 session,
     * 当用户注销登录后，session 就会失效，但是默认的失效是通过调用 StandardSession#invalidate 方法来实现的，
     * 这一个失效事件无法被 Spring 容器感知到，进而导致当用户注销登录后，Spring Security 没有及时清理会话信息表，
     * 以为用户还在线，进而导致用户无法登录重新登录
     * HttpSessionEventPublish 这个类实现了 HttpSessionListener 接口，在这个 Bean 中，可以将 session 的创建
     * 以及销毁的事件能够及时的感知到，并且调用 Spring 中的事件机制将相关的创建和销毁事件发布出去，进而被 Spring Security
     * 感知到
     * @return
     */
    @Bean
    HttpSessionEventPublisher httpSessionEventPublisher() {
        return new HttpSessionEventPublisher();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService);
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/js/**", "/css/**", "/images/**");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                // 配置角色权限拦截
                .antMatchers("/admin/**").hasRole("admin")
                .antMatchers("/user/**").hasRole("user")
                // 除了以上的两个拦截配置，以下的都只需要通过认证请求即可
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginProcessingUrl("/doLogin")
                .successHandler((res, resp, authentication) -> { // authentication 登录成功后保存的当前用户信息
                    // 成功的回调，每次登录成功后就不会跳转进入页面了而是直接返回一个 json
                    resp.setContentType("application/json;charset=utf-8");
                    PrintWriter out = resp.getWriter();
                    out.write(new ObjectMapper().writeValueAsString(authentication.getPrincipal()));
                    out.flush();
                    out.close();
                })
                .failureHandler((res, resp, exception) -> {
                    resp.setContentType("application/json;charset=utf-8");
                    PrintWriter out = resp.getWriter();
                    out.write(new ObjectMapper().writeValueAsString(exception.getMessage()));
                    out.flush();
                    out.close();
                })
                .permitAll()
                .and()
                .logout()
                .logoutUrl("/logout")
                .logoutSuccessHandler((res, resp, authentication) -> {
                    resp.setContentType("application/json;charset=utf-8");
                    PrintWriter out = resp.getWriter();
                    out.write(new ObjectMapper().writeValueAsString("注销登录成功!"));
                    out.flush();
                    out.close();
                })
                .permitAll()
                .and()
                .csrf().disable()
                .exceptionHandling()
                .authenticationEntryPoint((res, resp, exception) -> {
                    resp.setContentType("application/json;charset=utf-8");
                    PrintWriter out = resp.getWriter();
                    out.write(new ObjectMapper().writeValueAsString("尚未登录，请登录!"));
                    out.flush();
                    out.close();
                })
                .and()
                .sessionManagement()
                .maximumSessions(1) // 踢掉已登录的会话，如在 Chrome 已经登录，在 Firefox 中再次登录成功后，回到 Chrome 中访问接口会提示会话已过期
                .maxSessionsPreventsLogin(true); // 不会踢掉已登录的会话，如果已经登录了则在会话过期之前会阻止新的会话进行登录，一个浏览器登录成功后则另一个浏览器就无法登录了
    }

}
